"""
@author     : 大山
@Time       : 2022-4-21 20:48
@File       : jxsggzy-xpath-save-json.py
@Software   : PyCharm
@Desc       : 单/多进程通过xapth提取网页数据保存存为json
"""
import requests
import logging
import re
import json
import time
import multiprocessing
from lxml import etree
from os import makedirs
from os.path import exists
from urllib.parse import urljoin

# 所有网页都从这个链接开始
BASE_URL = 'https://www.jxsggzy.cn'
# 祥情页基础URL + （1-n.html）
PAGE_URL = 'https://www.jxsggzy.cn/web/jyxx/002001/002001001/'
# 预备爬取10个列表页面
TOTAL_PAGE = 10
# 保存JSON数据的文件夹，默认为当前文件夹下的results目录中
RESULT_DIR = 'results'
# 判断上面的文件夹是否存在，如果不存在则创建
exists(RESULT_DIR) or makedirs(RESULT_DIR)

# 定义headers头信息
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko'
}

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s: %(message)s')


def scrape_page(url):
    """
    获取URL地址的网页HTML文本并返回。它是一个公用函数，详情页和列表页都用该方法来获取网页HTML文本
    :param url: 网页地址
    :return: 返回请求得到的HTML文本
    """
    logging.info('爬取列表页 %s ....', url)
    try:
        response = requests.get(url=url, headers=headers, timeout=(5, 8))
        if response.status_code == 200:
            return response.content.decode()
        logging.error('get invalid status code %s while scraping %s', response.status_code, url)
    except requests.RequestException:
        logging.info('error occurred while scraping %s', url, exc_info=True)


def scrape_index(page):
    """
    爬取列表页的HTML文本，用上面的共用函数，首先生成列表页的URL(通过PAGE_URL + page + .html),传递给scrape_page方法
    :param page: 通过分析列表页得到列表页的URL规律，PAGE_URL + page,page在main函数中循环1-10传递page参数。
    :return: 返回HTML的文本代码
    """
    index_url = f'{PAGE_URL}{page}.html'
    return scrape_page(index_url)


def scrape_detail(url):
    """
    爬取详情页，通过上面的公用爬取页面HTMl代码的函数。为什么要定义这个函数了，直接传详情页的URL给上面的函数不就好了吗？这样做到层次分明
    :param url: 详情页的url
    :return: 返回HTMl页面的文本代码。
    """
    return scrape_page(url)


def parse_index(html):
    """
    解析列表页，获取每个详情页的url并返回
    :param html: 列表页的HTML文本代码
    :return: 用yield来生成一个生成器，并返回给调用者。生成器：可以迭代，它本身是不保存数据，只保存算法。这样做的目的是为了节省内存空间。
    （假如这里用函数中的return来返回，它务必要把所有详情页的URL放到一个集合中，并返回给调用者。假如这个集合的数据量非常大的话，它就会占用大量的内存空间）
    生成式用法通俗理解：在秦朝，都是用铜钱作为流通货币，它的特点是很重。假如你要出远门采购货物，需带上大量的铜钱。在路上很不方便，且容易被强盗惦记。你找我要了1000万（即数据），肯定带着不方便的。
    我给你的不是1000万的铜钱，而是给你了一套非常简单的造铜钱的方法且该方法只能造出1000万铜钱。你带着这个造铜钱的方法到山东采购茶叶，用这个方法造了200万铜钱并采购了茶叶。那么，200万被用于采购茶叶且花完了，不会在有了.
    到了北京，你又需要500万采购了烤鸭，你又用我给你的造铜钱的方法造了500万铜钱，购卖了烤鸭。这500万也没有了。下一站到四川，你需要采购300万的腊肉，你又用我的方法造了300万铜钱用于采购了腊肉，钱又没有了。但你临时还想去
    湖北采购一批热干面。但是，你用我给你造铜钱的方法已经造不出更多的铜钱，当你想用这个方法继续造钱时，这个方法就告诉你说，【你不能在造更多的铜钱了StopIteration】。因为，我给你造铜钱的方法，只能造出1000万。
    至此，你就可以带着所有采购的货物回老家了，采购任务结束。
    """
    html = etree.HTML(html)
    items = html.xpath('//a[@class="ewb-list-name"]/@href')
    if not items:  # 如果解析不到数据，则返回一个空列表
        return []
    for item in items:
        detail_url = urljoin(BASE_URL, item)
        logging.info('获取当前列表页中所有详情页的url: %s', detail_url)
        yield detail_url


def parse_text_form_exg(reg, text):
    """
    对具体数据格式进行补充解析(通过xpath未能完全解析的数据，通过该函数二次处理)
    :param reg: 正则表达式
    :param text: 待处理的文本
    :return: 返回匹配出来的字符串
    """
    if text is None:
        return None
    else:
        return re.search(reg, text).group(1)


def parse_detail(html):
    """
    解析详情页，提取数据
    :param html: 详情页的HTML文本
    :return: 返回字典格式数据
    """
    html = etree.HTML(html)
    # 项目名称的xpath表达式（[地区]+项目名称）
    pn_xpath_express = r'//div[@class="article-info"]/h1/text()'
    # 是否是变更公告，如果是就有值，如果否就无值
    m_xpath_express = r'//div[@class="article-info"]/h1/font/text()'
    # 公告发布日期
    update_xpath_express = r'//div[@class="article-info"]//p[@class="infotime"]/text()'
    # 招投标格式
    format_xpath_express = r'//div[@style="text-align: right;"]//span//text()'
    # 通过xpath提取出项目名称+地区+如果是变更公告则有值
    title = html.xpath(pn_xpath_express)[0]
    # 通过xpath提取出变更公告，如果有
    is_notice = html.xpath(m_xpath_express)[0] if html.xpath(m_xpath_express) else None
    # 提取公告发布日期
    release_date = html.xpath(update_xpath_express)[0]
    # 提取招投标格式，如果有，无则返回None
    text_format = html.xpath(format_xpath_express)[0] if html.xpath(format_xpath_express) else None

    name_of_project = parse_text_form_exg(r'^\W.*?\W(.*)', title)  # 项目名称
    location_of_project = parse_text_form_exg(r'\[(.+?)\]', title)  # 工程所属地
    notice_date = parse_text_form_exg(r'(\d.+\d)', release_date)  # 公告发布日期
    notice_type = parse_text_form_exg(r'\[(.+?)\]', is_notice)  # 是招标公告，还是更变更公告
    format_text = text_format  # 招投标格式

    return {
        'nameofproject': name_of_project,
        'area': location_of_project,
        'noticedate': notice_date,
        'type': notice_type,
        'format': format_text
    }


def save_data(data):
    """
    保存数据到文件夹中
    :param data: 解析出来的字典数据
    :return:
    """
    name = data.get('nameofproject')
    data_path = f'{RESULT_DIR}/{name}.json'
    json.dump(data, open(data_path, 'w', encoding='utf-8'), ensure_ascii=False, indent=2)


"""
单进程----开始
"""
# def main():
#     for page in range(1, TOTAL_PAGE + 1):
#         list_html = scrape_index(page)
#         detail_urls = parse_index(list_html)
#         for detail_url in detail_urls:
#             detail_html = scrape_detail(detail_url)
#             data = parse_detail(detail_html)
#             data['currurl'] = detail_url  # 当前URL地址
#             logging.info('获取详情页数据 %s', data)
#             logging.info('保存数据到json文本')
#             save_data(data)
#             logging.info('数据保存成功！')
#
#
# if __name__ == '__main__':
#     start_time = time.time()
#     main()
#     end_time = time.time()
#     print('程序运行时长：', end_time - start_time, '秒')
"""
单进程----结束
"""
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++


"""
多进程----开始
"""


def main(page):
    index_html = scrape_index(page)
    detail_urls = parse_index(index_html)
    for detail_url in detail_urls:
        detail_html = scrape_detail(detail_url)
        data = parse_detail(detail_html)
        data['currurl'] = detail_url  # 当前URL地址
        logging.info('获取详情页数据 %s', data)
        logging.info('保存数据到json文本')
        save_data(data)
        logging.info('数据保存成功！')


if __name__ == '__main__':
    start_time = time.time()
    pool = multiprocessing.Pool()
    pages = range(1, TOTAL_PAGE + 1)
    pool.map(main, pages)
    pool.close()
    pool.join()
    end_time = time.time()
    print('程序运行时长：', end_time - start_time, '秒')
"""
多进程----结束
"""
